#!/usr/bin/python3

# Copyright (c) 2020-2021 Fpemud <fpemud@sina.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.


import os
import subprocess
from .util import Util, PartiUtil, PhysicalDiskMounts
from .const import MOUNT_DIR, PartitionTableType, FileSystemType, CompatibleOs
from .errors import CreateError, CreateErrorNoValidPartitionTableType, CreateErrorNoValidFileSystemType, AccessError
from .backend_raw import Handy


class DataDisk:

    def __init__(self, devPath, partitionTableType, fs, fsUuid, label, mountEntry):
        assert PartiUtil.isDiskOrParti(devPath)
        assert partitionTableType in PartitionTableType
        assert fs in FileSystemType
        assert fsUuid is not None
        assert label is not None

        self._devPath = devPath
        self._partitionTableType = partitionTableType
        self._fs = fs
        self._fsUuid = fsUuid
        self._label = label
        self._mountEntry = mountEntry

    @property
    def dev_path(self):
        return self._devPath

    @property
    def partition_table_type(self):
        return self._partitionTableType

    @property
    def fs(self):
        return self._fs

    @property
    def fs_uuid(self):
        return self._fsUuid

    @property
    def label(self):
        return self._label

    def is_read_only(self):
        # FIXME: the block device is read-only
        return False

    def get_partition_dev_path(self):
        return self._getPartiDevPath()

    def get_mount_point(self):
        if self._mountEntry is not None:
            return self._mountEntry.mountpoint
        else:
            return None

    def get_mount_flags(self):
        assert self._mountEntry is not None

        ret = {}

        # flag: mode
        if "ro" in self._mountEntry.mnt_opt_list:
            ret["mode"] = "ro"
        elif "rw" in self._mountEntry.mnt_opt_list:
            ret["mode"] = "rw"
        else:
            assert False

        # FIXME
        # flag: encoding
        ret["encoding"] = None

        return ret

    def access(self, read_only):
        # already mounted
        if self._mountEntry is not None:
            if not read_only and self.get_mount_flags()["mode"] == "ro":
                raise AccessError("the data disk has been mounted read-only")
            if read_only and self.get_mount_flags()["mode"] == "rw":
                raise AccessError("the data disk has been mounted read-write")
            return self._mountEntry.mountpoint

        # check
        if self.is_read_only() and not read_only:
            raise AccessError("the data disk is read-only")

        # FIXME: should use gvfs
        mountDir = os.path.join(MOUNT_DIR, self._fsUuid)
        os.makedirs(mountDir, exist_ok=True)
        try:
            cmd = ["mount"]
            # if options is not None:
            #     cmd.append("-o")
            #     cmd.append(options)
            cmd.append(self._getPartiDevPath())
            cmd.append(mountDir)
            subprocess.check_call(cmd)
        except BaseException:
            os.rmdir(mountDir)
            raise

        # FIXME: maybe we should not use PhysicalDiskMounts
        for entry in PhysicalDiskMounts.get_entries():
            if entry.mountpoint == mountDir:
                assert entry.device == self._getPartiDevPath()
                self._mountEntry = entry
                break

        return self._mountEntry.mountpoint

    def eject(self):
        subprocess.check_call(["eject", self._devPath])

        if self._mountEntry is not None:
            mountDir = os.path.join(MOUNT_DIR, self._fsUuid)
            os.rmdir(mountDir)
            self._mountEntry = None

    def reformat(self, label, compatible_os_list, wipe=False):
        # FIXME: what to do if exception occurs

        partiDevPath = self._getPartiDevPath()
        lastMountDir = None

        # umount first
        if self._mountEntry is not None:
            lastMountDir = os.path.basename(self._mountEntry.mountpoint)
            subprocess.check_call(["umount", partiDevPath])
            self._mountEntry = None

        # get partition table type
        partitionTableType = CompatibleOs.get_partition_table_type_for_compatible_os_list(compatible_os_list)
        if partitionTableType is None:
            raise CreateErrorNoValidPartitionTableType(self._devPath)

        # get file system type
        fs = CompatibleOs.get_file_system_type_for_compatible_os_list(compatible_os_list)
        if fs is None:
            raise CreateErrorNoValidFileSystemType(self._devPath)

        # partitioning target device
        Util.initializeDisk(self._devPath, partitionTableType.value, [
            ("*", fs.value),
        ])

        # creating file system on partition 1
        subprocess.check_output(["mkfs.vfat", "-n", label, partiDevPath])

        # update variable
        self._partitionTableType = partitionTableType
        self._fs = fs
        self._fsUuid = Util.getBlkDevFsUuid(partiDevPath)
        self._label = label

        # re-mount if needed
        if lastMountDir is not None:
            cmd = ["mount"]
            # if options is not None:
            #     cmd.append("-o")
            #     cmd.append(options)
            cmd.append(partiDevPath)
            cmd.append(lastMountDir)
            subprocess.check_call(cmd)

            # FIXME: maybe we should not use PhysicalDiskMounts
            self._mountEntry = PhysicalDiskMounts.get_single_entry(partiDevPath, lastMountDir)
            assert self._mountEntry is not None

    def get_warnings(self):
        assert False

    def _getPartiDevPath(self):
        return PartiUtil.diskToParti(self._devPath, 1)


def probe_data_disks():
    ret = []

    for devPath in Handy.getDiskDevPathList():
        params = Handy.getDataDiskParam(devPath)
        if params is None:
            continue
        partiTableType, fs, fsUuid, label = params

        mountEntry = PhysicalDiskMounts.get_single_entry(devPath, os.path.join(MOUNT_DIR, fsUuid))

        ret.append(DataDisk(devPath, partiTableType, fs, fsUuid, label, mountEntry))

    return ret


def create_data_disk(dev_path, label, compatible_os_list):
    if not PartiUtil.inScope(dev_path):
        raise CreateError(dev_path, "Device %s is not supported." % (dev_path))

    # check
    if Handy.getDataDiskParam(dev_path) is not None:
        raise CreateError(dev_path, "Device %s is already a data disk, please use \"datadisks reformat\"." % (dev_path))

    # check
    for entry in PhysicalDiskMounts.get_entries():
        if not PartiUtil.inScope(entry.device) or PartiUtil.isDiskOrParti(entry.device):
            d = entry.device
        else:
            d = PartiUtil.partiToDisk(entry.device)
        if d == dev_path:
            raise CreateError(dev_path, "Device %s is mounted, please umount it first." % (dev_path))

    # get partition table type
    partitionTableType = CompatibleOs.get_partition_table_type_for_compatible_os_list(compatible_os_list)
    if partitionTableType is None:
        raise CreateErrorNoValidPartitionTableType(dev_path)

    # get file system type
    fs = CompatibleOs.get_file_system_type_for_compatible_os_list(compatible_os_list)
    if fs is None:
        raise CreateErrorNoValidFileSystemType(dev_path)
    if fs == FileSystemType.FAT:
        if label.upper() != label:
            raise CreateError(dev_path, "Label should not contain lower case letters for %s filesystem." % (FileSystemType.FAT))

    # partitioning target device
    Util.initializeDisk(dev_path, partitionTableType.value, [
        ("*", fs.value),
    ])
    partiDevPath = PartiUtil.diskToParti(dev_path, 1)

    # creating file system on partition 1
    subprocess.check_output(["mkfs.vfat", "-n", label, partiDevPath])

    return DataDisk(dev_path, partitionTableType, fs, Util.getBlkDevFsUuid(partiDevPath), label, None)
